/*
 * This file is Copyright 2002-2006 Loyola University Chicago,
 * Department of Computer Science and the Emerging Technologies Laboratory.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License via
 *    http://www.apache.org/licenses/LICENSE-2.0.
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.citep.web.admin.accounts.server;

import java.util.Date;

import com.google.gwt.user.server.rpc.RemoteServiceServlet;

import javax.servlet.*;
import javax.servlet.http.*;
import com.citep.web.admin.accounts.client.AccountService;
import com.citep.web.gwt.dto.*;

import com.citep.business.*;
import com.citep.business.sessions.*;
import com.citep.formats.input.accounts.AccountImportContext;
import com.citep.formats.input.transactions.ImportedStatement;
import com.citep.formats.input.transactions.ImportedEntry;
import com.citep.formats.input.ImportFileTypes;
import com.citep.formats.input.ImportException;

import java.math.BigDecimal;
import java.util.List;
import java.util.ArrayList;

import org.springframework.context.*;
import org.springframework.context.support.*;
import org.springframework.beans.BeanUtils;

public class AccountServiceImpl extends RemoteServiceServlet implements AccountService, SessionLabels {


	ApplicationContext appContext;
	AccountSession accountSession;
	TransactionSession transactionSession;
	UserSession userSession;
	
	AccountImportContext accountImportCtx;
	
	User authUser;
	
	public void init(ServletConfig config) throws ServletException {
		super.init(config);
		
		
		appContext = new ClassPathXmlApplicationContext("applicationContext.xml");
		
		//get our session beans from the application context
		accountSession = (AccountSession)appContext.getBean("accountSession");
		transactionSession = (TransactionSession)appContext.getBean("transactionSession");
		userSession = (UserSession)appContext.getBean("userSession");
		accountImportCtx = (AccountImportContext)appContext.getBean("accountImport");

		//populateTest();
	}

	private void populateTest() {
		//populate some accounts for testing purposes
		//create some sample accounts
		Account parent = accountSession.createAccount("root", "root account", AccountTypes.EQUITY, new BigDecimal("0.00"), new Date("1/1/2000"), new Date("1/2/2000"), true, accountSession.createAccountCategory("parent", "parent"));
		
		//create the "unbalanced account" (hidden)
		accountSession.createAccount("UNBALANCED", "non-balanced transactions", AccountTypes.ASSETS, new BigDecimal("0.00"), new Date("1/1/2000"), new Date("1/2/2000"), true, accountSession.createAccountCategory("unbalanced", "unbalanced"), parent);
		
		userSession.createRole("Administrator", "Administrator Role", true);
		userSession.createRole("User", "User Role", true);
		userSession.createRole("Guest", "User Role", true);
		userSession.createUser("Test", "User", "demo", "admin@citep.com",userSession.findRole("Administrator"));
		
		//import some accounts from gnucash (new!)
		try {
			accountImportCtx.importFile("/home/lurker/import.gnucash", ImportFileTypes.GNUCASH_ACCOUNTS);
		} catch(ImportException e) {
			System.out.println("init: " + e);
		}
		
	}
	
	protected User getAuthenticatedUser() {
	
		HttpSession session = this.getThreadLocalRequest().getSession();
		return (User)session.getAttribute(AUTHENTICATED_USER);
	}


	
	
	protected void validationExceptionToDTO(ValidationException e) {
		GWTValidationException exception = new GWTValidationException();
		exception.setFieldsTable(e.getFieldsTable());
		exception.setStackTrace(e.getStackTrace());
		System.out.println("throwing exception: "+ exception.getMessage());
		throw exception;
	}

	public void saveUser(UserDTO user) throws CitepException, GWTValidationException {
		try {
			
			//get the user stored in the database
			User originalUser = userSession.getUser(user.getId());
	
			//update its fields with those of the DTO object
			DTOUtils.DTOtoUser(user, originalUser);
			Role r = userSession.findRole(user.getRoleName());
			originalUser.setRole(r);
			
			System.out.println("Updating user");
			userSession.updateUser(originalUser);
		} catch (Throwable t) {
			System.out.println("oops");
			t.printStackTrace();
		}		
	}
	
	public void saveAccount(AccountDTO account, AccountDTO parentDTO) throws CitepException,GWTValidationException {
		try {
		
			//get the account stored in the database
			Account originalAccount = accountSession.getAccount(account.getId());
			//update its fields with those of the DTO object
			DTOUtils.DTOtoAccount(account,originalAccount);
			
			//get the parent account that is to be assigned to this account
			Account parent = null;
			if(parentDTO != null) {
				parent = accountSession.getAccount(parentDTO.getId());
			}
			
			originalAccount.setParent(parent);
			
			System.out.println("Updating account");
			accountSession.updateAccount(originalAccount);
		} catch (Throwable t) {
			System.out.println("oops");
			t.printStackTrace();
		}

	}
	
	
	public AccountDTO createAccount(String name, String description, String type, int accountCategoryId, String balance, Date startPeriod, Date endPeriod, boolean enabled, AccountDTO parentDTO)  throws CitepException,GWTValidationException {
		try {
			
			System.out.println("creating category");
			AccountCategory category = accountSession.getAccountCategory(accountCategoryId);
			
			//our account type enumeration is in uppercase, we need to convert this before calling Enum.valueOf
			type = type.toUpperCase();
			
			Account parent = null;
			if(parentDTO != null) {
				parent = accountSession.getAccount(parentDTO.getId());
			}
			
			Account account = accountSession.createAccount(name, description, AccountTypes.valueOf(type), new BigDecimal(balance), startPeriod, endPeriod, enabled, category, parent);
			
			AccountDTO createdAccount = new AccountDTO();
			
			DTOUtils.accountToDTO(account,createdAccount);
	
			accountSession.flush();

			return createdAccount;

		} catch(ValidationException e) {
			validationExceptionToDTO(e);
			return null;
		} catch (Throwable t) {
			System.out.println("oops");
			t.printStackTrace();
			return null;
		}
		
	}
	
	public AccountCategoryDTO createAccountCategory(String name, String description)  throws CitepException,GWTValidationException {
		try {
			
			AccountCategory category = accountSession.createAccountCategory(name,description);
			AccountCategoryDTO categoryDTO = new AccountCategoryDTO();
			BeanUtils.copyProperties(category, categoryDTO);
			return categoryDTO;
		} catch(ValidationException e) {
			validationExceptionToDTO(e);
			return null;
		} catch(Throwable t) {
			System.out.println("oops");
			t.printStackTrace();
			return null;
		}
	}
	
	public AccountDTO getAccount(int accountId)  throws CitepException {

		try {

			Account account = accountSession.getAccount(accountId);
			AccountDTO retrievedAccount = new AccountDTO();

			DTOUtils.accountToDTO(account,retrievedAccount);
			
			accountSession.flush();
			return retrievedAccount;
		} catch(Throwable t) {
			System.out.println("oops");
			t.printStackTrace();
			return null;
		}
	}

	
	public ArrayList getAccounts(int offset, int limit)  throws CitepException {

		ArrayList accountList = new ArrayList();
		
		try {
			List l = accountSession.getAccounts(offset, limit);
			for(int i = 0; i < l.size(); i++) {
				Account account = (Account)l.get(i);

				AccountDTO retrievedAccount = new AccountDTO();
				DTOUtils.accountToDTO(account,retrievedAccount);
				
				accountList.add(retrievedAccount);				
			}
		} catch(Throwable t) {
			System.out.println("oops");
			t.printStackTrace();			
		}
		
		return accountList;
	}

	private void populateChildren(Account node, AccountTreeNode treeNode) {

		treeNode.setAccount(DTOUtils.accountToDTO(node));
		
		if(node.hasChildren()) {
			for(Account child:node.getChildren()) {
				AccountTreeNode dtoChild = new AccountTreeNode();
				dtoChild.setParent(treeNode);
				treeNode.addChild(dtoChild);
				
				populateChildren(child,dtoChild);
			}
		}
	}
	
	private void populateAncestors(Account node, AccountTreeNode treeNode) {
		treeNode.setAccount(DTOUtils.accountToDTO(node));
		
		while(node.hasParent()) {
			AccountTreeNode dtoParent = new AccountTreeNode();
			dtoParent.setParent(treeNode);
			
			populateAncestors(node.getParent(),dtoParent);
		}
	}
	
	public AccountTreeNode getAccountChildrenTree(AccountDTO account) throws CitepException {
		
		if(account == null)
			throw new CitepException("Cannot retrieve the children of a null account");
		
		Account root = accountSession.getAccountTree(account.getId());

		AccountTreeNode start = new AccountTreeNode();
		populateChildren(root, start);
		return start;
	}

	public AccountTreeNode getAccountParentHierarchy(AccountDTO account) throws CitepException {
		if(account == null)
			throw new CitepException("Cannot retrieve the children of a null account");
		
		Account root = accountSession.getAccount(account.getId());
		
		AccountTreeNode start = new AccountTreeNode();
		populateAncestors(root,start);
		
		return start;
	}

	public AccountDTO getRoot() throws CitepException {
		Account root = accountSession.getRoot();
		
		if(root != null) {
			AccountDTO dto = DTOUtils.accountToDTO(root);
			dto.setFqName(root.getName());
			return dto;
		} else {
			return null;
		}
	}

	public AccountTreeNode getAccountTree() throws CitepException {
		AccountDTO root = getRoot(); 

		if(root == null)
			return null;
		
		HttpSession session = this.getThreadLocalRequest().getSession();
		session.setAttribute(ACCOUNTS_LAST_MODIFIED, accountSession.getAccountTreeLastModified());
		return getAccountChildrenTree(root);
	}
	
	//returns true if we have the latest copy of the account tree, based on the
	//parameter date
	public Boolean isAccountTreeSynched() {
		Date stored = (Date)this.getThreadLocalRequest().getSession().getAttribute(ACCOUNTS_LAST_MODIFIED);
		
		if(stored == null)
			return false;
		
		Date current = accountSession.getAccountTreeLastModified();
		return current == null || current.equals(stored);
	}
	
	
	public ArrayList getAccountCategories()  throws CitepException {
		ArrayList categoryList = new ArrayList();

		try {
			List l = accountSession.getAccountCategories();
			for(int i = 0; i < l.size(); i++) {
				AccountCategory category = (AccountCategory)l.get(i);
				
				AccountCategoryDTO retrievedCategory = new AccountCategoryDTO();
				BeanUtils.copyProperties(category, retrievedCategory);
				
				categoryList.add(retrievedCategory);
			}
		} catch(Throwable t) {
			System.out.println("oops");
			t.printStackTrace();
		}
		
		return categoryList;
	}
	
	public ArrayList getEntryTypes() {
		ArrayList typeList = new ArrayList();
		for(EntryTypes entryType:EntryTypes.values())
			typeList.add(entryType.toString());
		
		return typeList;
	}
	
	public void createTransaction(AccountDTO source, AccountDTO dest, String entryType, String memo, Date transactionDate, String amount) throws CitepException,GWTValidationException {

		try {
			Account sourceDB = accountSession.getAccount(source.getId());
			Account destDB = accountSession.getAccount(dest.getId());
			
			EntryTypes type = EntryTypes.valueOf(entryType);
			
			transactionSession.transfer(getAuthenticatedUser(), sourceDB, destDB, type, memo, transactionDate, new BigDecimal(amount));
			
		} catch(Throwable t) {
			System.out.println("oops");
			t.printStackTrace();
			
		}

	}
	
	public ArrayList getAccountEntries(AccountDTO a, int offset, int limit) {
		ArrayList entryList = new ArrayList();
		
		try {
			Account account = accountSession.getAccount(a.getId());
			
			List<Entry> entries = transactionSession.getEntries(account, offset, limit);
			
			
			
			
			
			for(Entry e:entries) {
				EntryInfo entryInfo = new EntryInfo();
				DTOUtils.EntryToDTO(e, entryInfo.getEntry());
				entryInfo.setDate(new Date(e.getTransaction().getTransactionTime().getTime()));
				entryList.add(entryInfo);
			}
			
			
			
			//get the balance of the last entry
			int listSize = entries.size();
			
			BigDecimal balance = new BigDecimal("0.00");
			
			if(listSize > 0) {
				Entry e = entries.get(listSize - 1);

				balance = transactionSession.getAccountBalance(account, e.getId());
				
				//set the balance of the last entry = lastBalance
				EntryInfo entryInfo = (EntryInfo)entryList.get(listSize - 1);
				entryInfo.setBalance(balance.toString());
				
				//substract this entry's amount from the balance, so that the next
				//entry gets the correct balance
				balance = balance.subtract(e.getAmount());
			}
			
			
			//go backwards and setup the balances of the rest of the entries
			for(int i = listSize - 2; i >= 0; i--) {
				EntryInfo entryInfo = (EntryInfo)entryList.get(i);
				Entry entry = entries.get(i);
				
				entryInfo.setBalance(balance.toString());

				//substract this entry's amount from the balance, so that the next
				//entry gets the correct balance
				balance = balance.subtract(entry.getAmount());
			}
			
			
			
		} catch(Throwable t) {
			System.out.println("oops");
			t.printStackTrace();
		}
		
		return entryList;
	}

	public String getAccountBalance(AccountDTO a, EntryDTO e) {
		
		Account account = accountSession.getAccount(a.getId());
		
		BigDecimal balance = transactionSession.getAccountBalance(account, e.getId());
		
		return balance.toString();
	}
	
	public UserDTO createUser(String firstName, String lastName, String email, String password, int roleId) {
		Role role = userSession.getRole(roleId);
		User user = userSession.createUser(firstName, lastName, password, email, role);
		
		UserDTO userDTO = new UserDTO();
		
		DTOUtils.UserToDTO(user, userDTO);
		
		return userDTO;
	}

	public ArrayList getRoles() throws CitepException {
		ArrayList roleList = new ArrayList();
		
		try {
			List<Role> roles = userSession.getRoles();
			for(Role r:roles) {
				RoleDTO dto = new RoleDTO();
				DTOUtils.RoleToDTO(r, dto);
				roleList.add(dto);
			}
		} catch (Throwable t) {
			System.out.println("oops");
			t.printStackTrace();
		}
		
		return roleList;
	}
	
	public ArrayList getUsers(int offset, int limit) throws CitepException {
		ArrayList userList = new ArrayList();
		
		try {
			List<User> users = userSession.getUsers(offset, limit);
			
			for(User u:users) {
				UserDTO dto = new UserDTO();
				DTOUtils.UserToDTO(u, dto);
				userList.add(dto);
			}
		} catch (Throwable t) {
			System.out.println("oops");
			t.printStackTrace();
		}
		
		return userList;
	}
	
	public UserDTO getUser(int userId) {
		try {
			User u = userSession.getUser(userId);
			UserDTO dto = new UserDTO();
			DTOUtils.UserToDTO(u, dto);
			return dto;
		} catch (Throwable t) {
			System.out.println("oops");
			t.printStackTrace();
			return null;
		}
	}

	public Date getLatestTransactionTime() {
		Date date = transactionSession.getLatestTransactionTime();
		if(date != null)
			date = new Date(date.getTime());
		return date;
	}

	public Date getLatestTransactionTime(AccountDTO account) {
		Account original = accountSession.getAccount(account.getId());
		Date date = transactionSession.getLatestTransactionTime(original);
		if(date != null)
			date = new Date(date.getTime());
		return date;
	}
	
	public UserDTO login(String email, String password) {
		try {
			User u = userSession.findUser(email,password);
			if(u == null)
				return null;
			
			HttpSession session = this.getThreadLocalRequest().getSession();
			session.setAttribute(AUTHENTICATED_USER, u);
			
			UserDTO dto = new UserDTO();
			DTOUtils.UserToDTO(u, dto);
			return dto;
		} catch (Throwable t) {
			System.out.println("oops");
			t.printStackTrace();
			return null;
		}
	}

	public StatementDTO getImportedTransactions(int statementNumber) throws CitepException {
		HttpSession session = this.getThreadLocalRequest().getSession();
		ArrayList<ImportedStatement> statements = (ArrayList<ImportedStatement>)session.getAttribute(IMPORTED_STATEMENTS);

		System.out.println("Statements in session:" + statements);
		
		if(statementNumber < 0 || statements == null || statements.size() < statementNumber)
			throw new CitepException("Invalid statement id: " + statementNumber);
		
		ImportedStatement statement = statements.get(statementNumber);
		ArrayList<ImportedEntryDTO> entries = new ArrayList<ImportedEntryDTO>();

		for(ImportedEntry entry:statement.getEntries()) {
			ImportedEntryDTO importedEntry = new ImportedEntryDTO();
			importedEntry.setId(entry.getId());
			
			System.out.println("Entry amount: " + entry.getAmount());
			
			importedEntry.setAmount(entry.getAmount().toString());
			importedEntry.setMemo(entry.getMemo());
			importedEntry.setType(entry.getType().toString());
			importedEntry.setEntryDate(entry.getDate());
			importedEntry.setParentAccountName(entry.getAccount().getName());
			
			//if the user has already updated transactions on the browser,
			//they will be reflected here as well
			if(entry.getSource() != null) {
				System.out.println("Setting account:" +entry.getSource());
				importedEntry.setSourceAccountId(entry.getSource().getId());
			}
			
			entries.add(importedEntry);
		}
		
		StatementDTO statementDTO = new StatementDTO();
		statementDTO.setTransactions(entries);
		statementDTO.setAccountName(statement.getAccount().getName());
		
		return statementDTO;
	}
	
	public int getPendingStatementCount() {
		HttpSession session = this.getThreadLocalRequest().getSession();
		ArrayList<ImportedStatement> statements = (ArrayList<ImportedStatement>)session.getAttribute(IMPORTED_STATEMENTS);		

		if(statements == null)
			return 0;
		else
			return statements.size();
	}

	public void setImportedTransactionAccount(int statementNumber, int idx, AccountDTO account) {
		HttpSession session = this.getThreadLocalRequest().getSession();
		ArrayList<ImportedStatement> statements = (ArrayList<ImportedStatement>)session.getAttribute(IMPORTED_STATEMENTS);		

		if(statementNumber < 0 || statements == null || statements.size() < statementNumber)
			throw new CitepException("Invalid statement id: " + statementNumber);

		ImportedStatement statement = statements.get(statementNumber);
		ArrayList<ImportedEntryDTO> entries = new ArrayList<ImportedEntryDTO>();

		if(idx > statement.getEntries().size())
			throw new CitepException("Invalid entry index: " + idx);
			
		ImportedEntry entry = statement.getEntries().get(idx);
		
		Account original = accountSession.getAccount(account.getId());		
		System.out.println("setting entry: " + entry + " to  " + original);
		entry.setSource(original);
		
	}
	
	public void commitStatementImport(int statementNumber, AccountDTO account) {
		HttpSession session = this.getThreadLocalRequest().getSession();
		ArrayList<ImportedStatement> statements = (ArrayList<ImportedStatement>)session.getAttribute(IMPORTED_STATEMENTS);		

		if(statementNumber < 0 || statements == null || statements.size() < statementNumber)
			throw new CitepException("Invalid statement id: " + statementNumber);
		
		if(account == null)
			throw new CitepException("Invalid destination account (null)");
		
		Account dest = accountSession.getAccount(account.getId());
		
		if(dest == null)
			throw new CitepException("Could not find an account in the database with id="+account.getId());

		ImportedStatement statement = statements.get(statementNumber);
		ArrayList<ImportedEntryDTO> entries = new ArrayList<ImportedEntryDTO>();

		User user = (User)session.getAttribute(AUTHENTICATED_USER);
		
		for(ImportedEntry entry:statement.getEntries()) {
			transactionSession.transfer(user, entry.getSource(), dest, entry.getType(), entry.getMemo(), entry.getDate(), entry.getAmount().abs());
/*			
			if(entry.getType() == EntryTypes.DEBIT)
				transactionSession.transfer(user, entry.getSource(), dest, EntryTypes.CREDIT, entry.getMemo(), entry.getDate(), entry.getAmount().abs());
			else
				transactionSession.transfer(user, entry.getSource(), dest, EntryTypes.DEBIT, entry.getMemo(), entry.getDate(), entry.getAmount().abs());
*/
		}
		
		statements.remove(statementNumber);
		System.out.println("Statements: " + statementNumber);
	}
}
